package ext.trace.scene;

import ext.trace.geom.Geom;
import math.math;
import math.region.AABB3;
import math.region.Ray3;
import math.region.Surface3;
import math.vec3;
import math.vec3i;

public class AAGrid extends Scene
{
    
    private static final vec3i N = new vec3i(2);
    
    private static final vec3 Nf = new vec3(N);
    
    
    private Bucket[][][] mBuckets;
    
    private vec3 mBucketSize;
    
    private vec3 mMin;
    
    private vec3 mMax;
    
    
    public AAGrid(
            vec3 min, 
            vec3 max)
    {
        this.mBuckets = new Bucket[N.z][N.y][N.x];
        this.mBucketSize = vec3.sub(max, min);
        this.mBucketSize.div(AAGrid.Nf);
        
        vec3 bmin = new vec3();
        vec3 bmax = new vec3();
        for (int k = 0; k < N.z; k++)
        {
            bmin.z = min.z + this.mBucketSize.z * k;
            bmax.z = min.z + this.mBucketSize.z * (k + 1);
            for (int j = 0; j < N.y; j++)
            {
                bmin.y = min.y + this.mBucketSize.y * j;
                bmax.y = min.y + this.mBucketSize.y * (j + 1);
                for (int i = 0; i < N.x; i++)
                {
                    bmin.x = min.x + this.mBucketSize.x * i;
                    bmax.x = min.x + this.mBucketSize.x * (i + 1);
                    
                    AABB3 bounds = new AABB3(bmin, bmax);
                    this.mBuckets[k][j][i] = new Bucket(bounds);
                }
            }
        }
        this.mMin = min.copy();
        this.mMax = max.copy();
    }
    
    
    public void add(final Geom g)
    {
        super.add(g);
        // Bounding box around G including start and end states.
        AABB3 gBox = g.rSurf0.boundingBox();
        gBox.extendToContain(g.rSurf1.boundingBox());
        
        for (Bucket[][] bList2 : this.mBuckets)
        {
            for (Bucket[] bList1 : bList2)
            {
                for (Bucket bi : bList1)
                {
                    boolean overlap = gBox.overlaps(bi.mBox);
                    if (overlap)
                    {
                        bi.add(g);
                    }
                }
            }
        }
    }
    public void PRINT()
    {
//        int size = 0;
        for (Bucket[][] bList2 : this.mBuckets)
        {
            for (Bucket[] bList1 : bList2)
            {
                for (Bucket bi : bList1)
                {
//                    size += bi.mG.size();
                    System.out.println();
                    System.out.println(bi);
                }
            }
        }
//        System.out.println(size / vec3.dot(new vec3(1), Nf));
    }
    
    public double castOcclusion(
            Ray3 aRay, 
            Geom aTarget, 
            double time)
    {
        Hit hit = this.castRay(aRay, time);
        if (hit != null)// && hit.rGeom == aTarget)
        {
            return 1.0;
        }
        return 1.0; // TODO: RETURN THIS TO 0.
    }
    
    
    public Hit castRay(Ray3 aRay, double aTime)
    {
        vec3i sign = new vec3i();
        sign.x = (aRay.mDir.x >= 0) ? 1 : -1;
        sign.y = (aRay.mDir.y >= 0) ? 1 : -1;
        sign.z = (aRay.mDir.z >= 0) ? 1 : -1;
        
        vec3 r = new vec3();
        r.x = (math.abs(aRay.mDir.x) < math.EPSILON) ? 1.0 / math.EPSILON : 1.0 / aRay.mDir.x;
        r.y = (math.abs(aRay.mDir.y) < math.EPSILON) ? 1.0 / math.EPSILON : 1.0 / aRay.mDir.y;
        r.z = (math.abs(aRay.mDir.z) < math.EPSILON) ? 1.0 / math.EPSILON : 1.0 / aRay.mDir.z;
        
        vec3 start = vec3.sub(aRay.mOrig, this.mMin);
        start.div(this.mBucketSize);
        
        vec3i posBi = new vec3i(start);
        posBi.x = math.clampi(posBi.x, 0, N.x);
        posBi.y = math.clampi(posBi.y, 0, N.y);
        posBi.z = math.clampi(posBi.z, 0, N.z);
        
        if (posBi.lessAny(vec3i.ZERO) || posBi.gequalAny(N)) // Outside grid.
        {
            System.out.println("Starting outside: " + posBi);
            return null;
        }
        
        vec3 deltaT = vec3.mul(this.mBucketSize, r); // units: length * (time / length) = time
        
        vec3 maxT = new vec3( // units: (time / length) * (length - length) = time
            r.x * (sign.x - start.x), 
            r.y * (sign.y - start.y), 
            r.z * (sign.z - start.z)
        );
//        maxT.mul(0.9);
        
        while (true)
        {
            if (posBi.lessAny(vec3i.ZERO) || posBi.gequalAny(N)) // Outside grid.
            {
//                System.out.println("Starting outside: " + posBi);
                return null;
            }
            
            Hit hit = this.visit(aRay, posBi, aTime);
            if (hit != null)
            {
                return hit;
            }
            
            if (maxT.x < maxT.y)
            {
                if (maxT.x < maxT.z)
                {
                    posBi.x += sign.x;
                    maxT.x += deltaT.x;
                }
                else // maxT.x >= maxT.z
                {
                    posBi.z += sign.z;
                    maxT.z += deltaT.z;
                }
            }
            else // maxT.x >= maxT.y
            {
                if (maxT.y < maxT.z)
                {
                    posBi.y += sign.y;
                    maxT.y += deltaT.y;
                }
                else // maxT.y >= maxT.z
                {
                    posBi.z += sign.z;
                    maxT.z += deltaT.z;
                }
            }
        }
    }
    
    
    private Hit visit(Ray3 aRay, vec3i aInd, double aTime)
    {
        Bucket bi = this.mBuckets[aInd.z][aInd.y][aInd.x];
        Hit hit = new Hit();
        hit.mT = Double.MAX_VALUE;
        for (Geom g : bi.mG)
        {
            Surface3 surf = g.lerp(aTime);
            double t = this.intersectSurface(surf, aRay);
            boolean validT = (t > math.EPSILON && t < Double.MAX_VALUE);
            boolean betterT = (t < hit.mT);
            if (validT && betterT)
            {
                hit.mT = t;
                hit.rPt = aRay.point(hit.mT);
                hit.rUV = surf.convertWorld_Param(hit.rPt);
                hit.rNorm = surf.surfaceNorm(hit.rPt, aRay.mOrig);
                hit.rColor = g.mMat.rBRDF.rAlbedo.lookupNN(hit.rUV);
                hit.rMat = g.mMat;
                hit.rSurface = surf;
            }
        }
        if (hit.rPt == null)
        {
            return null;
        }
        return hit;
    }
    
    private vec3i convertPos_GridIndex(vec3 pos)
    {
        final vec3 gridSize = vec3.sub(this.mMax, this.mMin);
        final vec3 indexRange = vec3.sub(AAGrid.Nf, 1.0);
        
        vec3 gridInd = vec3.sub(pos, this.mMin);
        gridInd.div(gridSize);
        gridInd.mul(indexRange);
        gridInd.add(0.5);
        return new vec3i(gridInd);
    }
    
    private vec3 convertGridIndex_Pos(vec3i index)
    {
        final vec3 gridSize = vec3.sub(this.mMax, this.mMin);
        final vec3 indexRange = vec3.sub(AAGrid.Nf, 1.0);
        
        vec3 pos = new vec3(index);
        pos.div(indexRange);
        pos.mul(gridSize);
        pos.add(this.mMin);
        return pos;
    }
    
}
